﻿/// -----------------------------------------------------------------------
/// (c) 2009 Microsoft Corporation -- All rights reserved
/// This code is licensed under the MS-PL
/// http://www.opensource.org/licenses/ms-pl.html
/// Courtesy of the Open Source Techology Center: http://port25.technet.com
/// -----------------------------------------------------------------------

// #define INTERACTIVE

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace SvnReverseProxy {
    public partial class SvnReverseProxyService : ServiceBase {
        internal const int MaxBufferSize = 128*1024; // 128k
        internal readonly Dictionary<string, byte[]> cachedStaticFiles = new Dictionary<string, byte[]>();
        internal string[] inlineRewrites;
        internal HttpListener listener;
        internal string htdocsDir;
        internal string confDir;
        internal string confFile;

        public SvnReverseProxyService() {
            InitializeComponent();
        }

        internal string[] InlineRewrites {
            get {
                if (inlineRewrites == null) {
                    inlineRewrites = Settings.Default.inlineRewrites.Split(',');
                }
                return inlineRewrites;
            }
        }

        protected override void OnStart(string[] args) {
            if(Init())
                new Thread(ProcessRequests).Start();
        }

        protected override void OnStop() {
            try {
                listener.Abort();
                listener.Stop();
            }
            catch( Exception ) {
                // who cares, we're goin' down.
            }
        }

        private bool Init() {
            htdocsDir = Path.Combine(Settings.Default.VisualSVNDir, "htdocs\\");
            confDir = Path.Combine(Settings.Default.VisualSVNDir, "conf\\");
            confFile = Path.Combine(confDir, "httpd-custom.conf");
            if(!File.Exists(confFile)) {
                return Fail("cannot find Apache configuration file at [{0}]", confFile);
            }

            var httpdCustomOpen = String.Format(@"<Location /{0}>",Settings.Default.SVNPrefix);
            var httpdCustomSetting =String.Format(@"SVNIndexXSLT ""/{0}svnindex.xsl"" ",Settings.Default.SVNPrefix);
            var httpdCustomClose = "</Location>";

            bool write = false;

            string[] confText =File.ReadAllLines(confFile);
            if( confText.Length != 0 ) {
                for(var i=0;i<confText.Length; i++) {
                    if( confText[i].Equals(httpdCustomOpen) ) {
                        // it has an entry for this svn instance
                        if(confText.Length < i+2)
                            return Fail("Apache custom configuration file has a location tag, but doesn't appear long enough for this silly parser.");
                        
                        if( confText[i+1].Equals(httpdCustomSetting))
                            return true;

                        confText[i + 1] = httpdCustomSetting;
                        write = true;
                    }

                }
            }
            List<string> txt = new List<string>(confText); 

            if( !write ) { // there was no existing entry. let's add one 
                txt.Add(httpdCustomOpen);
                txt.Add(httpdCustomSetting);
                txt.Add(httpdCustomClose);
            }
            
            try {
                File.WriteAllLines(confFile, txt.ToArray());
                
            }
            catch(Exception) {
                return Fail("Unable To Write out custom configuration to file [{0}]:\r\n{1}\r\n{2}\r\n{3}", confFile, httpdCustomOpen, httpdCustomSetting, httpdCustomClose);
            }

            try {

                ServiceController[] services = ServiceController.GetServices();
                foreach (ServiceController service in services) {
                    if (service.ServiceName == "VisualSVNServer") {
                        service.Stop();
                        service.Start();
                    }
                }
            }
            catch( Exception) {
                return Fail("Bad Mojo: had a problem restarting the VisualSVN service");
            }
            return true;
        }

        private bool Fail(string s, params object[] p) {
            var txt = string.Format(s, p);
#if INTERACTIVE
            Console.Error.WriteLine(txt);
#else
            EventLog.WriteEntry(txt, EventLogEntryType.Error);
#endif
            return false;
        }

        private void ProcessRequests() {
            var prefixes = new string[1];
            prefixes[0] = string.Format("{0}/{1}", Settings.Default.ListenPrefix, Settings.Default.SVNPrefix);

            // URI prefixes are required,
            if (prefixes == null || prefixes.Length == 0) {
                throw new ArgumentException("prefixes");
            }

            // Create a listener and add the prefixes.
            listener = new HttpListener();
            foreach (string s in prefixes) {
                listener.Prefixes.Add(s);
            }

            try {
                // Start the listener to begin listening for requests.
                listener.Start();
                Console.WriteLine("Listening...");

                listener.BeginGetContext(ListenerCallback, listener);

                while (listener.IsListening) {
                    listener.BeginGetContext(ListenerCallback, listener).AsyncWaitHandle.WaitOne();
                }
            }
            catch (HttpListenerException ex) {
                Console.WriteLine(ex.Message);
            }
            finally {
                // Stop listening for requests.
                listener.Close();
                Console.WriteLine("Done Listening.");
            }
        }

        // Summary:
        //     References a method to be called when a corresponding asynchronous operation
        //     completes.
        //
        // Parameters:
        //   ar:
        //     The result of the asynchronous operation.
        public void ListenerCallback(IAsyncResult ar) {
            HttpListenerContext context = ((HttpListener) ar.AsyncState).EndGetContext(ar);
            HttpListenerRequest request = context.Request;
            HttpListenerResponse response = context.Response;

            // info:
            string path = request.RawUrl;
            string httpMethod = request.HttpMethod;
            var binaryData = new byte[MaxBufferSize];

            try {
                Console.WriteLine(request.Url.PathAndQuery);

                // check our cached static files first.
                if (cachedStaticFiles.ContainsKey(request.Url.PathAndQuery)) {
                    byte[] cachedFile = cachedStaticFiles[request.Url.PathAndQuery];
                    response.OutputStream.Write(cachedFile, 0, cachedFile.Length);
                    return;
                }

                // we need to search for queries for files that are referenced as "/svn/<filename>.<ext>"
                // the segments property would be:
                // segments[0] == "/"
                // segments[1] == Settings.Default.SVNPrefix
                // segments[3] == a file existing in Settings.Default.VisualSVNDir
                var segments = request.Url.Segments;
                if (segments.Length == 3 && segments[1].Equals(Settings.Default.SVNPrefix)) {
                    var localFile = Path.Combine(htdocsDir, segments[2]);

                    if (File.Exists(localFile)) {
                        byte[] svnLocalFile;
                        if (InlineRewrites.Length == 3 && InlineRewrites[0].Equals(segments[2])) {
                            var text = Regex.Replace(File.ReadAllText(localFile), InlineRewrites[1],
                                                        InlineRewrites[2]);
                            svnLocalFile = Encoding.ASCII.GetBytes(text);
                        }
                        else {
                            svnLocalFile = File.ReadAllBytes(localFile);
                        }
                        cachedStaticFiles.Add(request.Url.PathAndQuery, svnLocalFile);
                        response.OutputStream.Write(svnLocalFile, 0, svnLocalFile.Length);
                        return;
                    }
                }

                // if( request.Url.PathAndQuery starts with??? )
                // Connect to destination server
                var upstreamConnection = new TcpClient();
                upstreamConnection.Connect(Settings.Default.SVNHostName, Settings.Default.SVNPort);

                NetworkStream upStream = upstreamConnection.GetStream();

                // send request to upstream server.
                NetworkStreamWriteLine(upStream, "{0} {1} HTTP/{2}.{3}", httpMethod, path, request.ProtocolVersion.Major,
                                       request.ProtocolVersion.Minor);


                // relay the headers
                foreach (string header in request.Headers.Keys) {
                    switch (header) {
                        case "Host":
                            NetworkStreamWriteLine(upStream, "Host: {0}:{1}", Settings.Default.SVNHostName,
                                                   Settings.Default.SVNPort);
                            break;
                        case "Authorization":
                            NetworkStreamWriteLine(upStream, "{0}: {1}", header, request.Headers[header]);
                            break;
                        case "Destination":
                            int slashCount = 0;
                            int characterPosition = 0;
                            string newDestination = request.Headers[header];

                            while (slashCount < 3 && characterPosition < newDestination.Length) {
                                if (newDestination[characterPosition++] == '/') {
                                    slashCount++;
                                }
                            }

                            NetworkStreamWriteLine(upStream, "{0}: {1}://{2}:{3}/{4}", header, "HTTP",
                                                   Settings.Default.SVNHostName, Settings.Default.SVNPort,
                                                   newDestination.Substring(characterPosition));
                            break;
                        default:
                            NetworkStreamWriteLine(upStream, "{0}: {1}", header, request.Headers[header]);
                            break;
                    }
                }

                // relay the body
                NetworkStreamWrite(upStream, "\r\n");
                var requestContentLength = (int) request.ContentLength64;
                while (requestContentLength > 0) {
                    int sz = request.InputStream.Read(binaryData, 0, MaxBufferSize);
                    upStream.Write(binaryData, 0, sz);
                    requestContentLength -= sz;
                }
                upStream.Flush();

                // read the upStream response
                binaryData = new byte[MaxBufferSize];
                int count = upStream.Read(binaryData, 0, binaryData.Length);
                int offset;
                int contentLength = 0;
                bool isChunked = false;

                // find response header break
                for (offset = 0; offset < count; offset++) {
                    if (binaryData[offset] == 10 && binaryData[offset + 2] == 10) {
                        break;
                    }
                }

                // parse upstream response header block
                var stream = new MemoryStream(binaryData, 0, offset + 2);

                using (var reader = new StreamReader(stream)) {
                    string line = reader.ReadLine();
                    string[] peices = line.Split(' ');

                    response.StatusCode = int.Parse(peices[1]);
                    response.StatusDescription = peices[2];

                    line = reader.ReadLine();
                    while (!string.IsNullOrEmpty(line)) {
                        int length = line.IndexOf(":");
                        string key = line.Substring(0, length).Trim();
                        string value = line.Substring(length + 1).Trim();
#if DEBUG
                        Log("Header [{0}]:[{1}]", key, value);
#endif
                        switch (key) {
                            case "Content-Length":
                                contentLength = int.Parse(value);

                                // response.AddHeader(key, value);
                                break;

                            case "Transfer-Encoding":
                                if (value == "chunked") {
                                    isChunked = true;
                                }
                                response.SendChunked = true;
                                break;

                            case "Location":
                                // write the real location.
                                try {
                                    response.AddHeader("Location", string.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Host, value.Substring(value.IndexOf('/', 7))));
                                } catch(Exception) {
                                    Log("BUG: The Location Header has been rejected [{0}://{1}{2}]\r\n\r\nLikely, this is because of a unicode character breaking something, and Addheader thinks it's a control character.", request.Url.Scheme, request.Url.Host, value.Substring(value.IndexOf('/', 7)));
                                    response.StatusCode = 500;
                                    response.StatusDescription = "BUG--I don't think it likes unicode file names.";
                                    response.Close();
                                    return;
                                }

#if DEBUG

                                Log("LocationHeader: {0}://{1}{2}", request.Url.Scheme, request.Url.Host, value.Substring(value.IndexOf('/', 7)));
#endif
                                break;

                            default:
                                response.AddHeader(key, value);
                                break;
                        }
                        line = reader.ReadLine();
                    }
                }

                // adjust the start of binary data
                offset += 3;
#if DEBUG
                Log("Before offset[{0}] count[{1}] left[{2}]", offset, count, count - offset);
#endif 
                if (!isChunked) {
                    if (contentLength == 0) {
                        response.ContentLength64 = 0;
                    }
                    else {
                        NormalOutput(upStream, response, binaryData, offset, count, contentLength);
                    }
                }
                else {
                    ChunkedOutput(upStream, response, binaryData, offset, count);
                }
            }
            catch (HttpListenerException ex) {
                Console.WriteLine(ex.Message);
                response = null;
            }
            finally {
                // Console.WriteLine("Closing Response");
                if (response != null) {
                    response.Close();
                }
            }
        }

        private void NormalOutput(NetworkStream networkStream, HttpListenerResponse Response, byte[] buffer,
                                         int bufferStartPointer, int bytesInBuffer, int completeContentLength) {
            int bytesLeft = bytesInBuffer - bufferStartPointer;
#if DEBUG
            Log("Normal Output: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] BytesLeft[{3}] ContentLength[{4}]", buffer.Length, bytesInBuffer, bufferStartPointer, bytesLeft, completeContentLength);
#endif 
            if (completeContentLength == 0) {
                completeContentLength = bytesLeft;
            }

            if (bytesLeft > completeContentLength) {
                throw new Exception( "ERROR STATE-There are more bytes in the buffer than the content-length should allow.");
            }

            Response.OutputStream.Write(buffer, bufferStartPointer, bytesLeft);
            completeContentLength -= bytesLeft;

            // get and send the rest.
            while (completeContentLength > 0) {
                bytesInBuffer = networkStream.Read(buffer, 0, buffer.Length);
                if (bytesInBuffer <= 0) {
                    throw new Exception(
                        "ERROR STATE-the upStream connection ran out of data before we sent enough bytes.");
                }

                Response.OutputStream.Write(buffer, 0, bytesInBuffer);
                completeContentLength -= bytesInBuffer;
            }
        }

        private void ChunkedOutput(NetworkStream networkStream, HttpListenerResponse Response, byte[] buffer,
                                          int bufferStartPointer, int bytesInBuffer) {
            // give the buffer to the intermediate stream
            int bytesToTransfer = -1;

            while (true) {
                if (bufferStartPointer >= bytesInBuffer) {
                    bytesInBuffer = networkStream.Read(buffer, 0, buffer.Length);
                    bufferStartPointer = 0;
                }
                int dataStartPointer = bufferStartPointer;

                while (buffer[dataStartPointer] != 10) {
                    dataStartPointer++;
                }

                int.TryParse(
                    Encoding.ASCII.GetString(buffer, bufferStartPointer, dataStartPointer - bufferStartPointer),
                    NumberStyles.HexNumber, null, out bytesToTransfer);
#if DEBUG
                Log("1 Chunk: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] Bytes to Transfer [{3}]",
                    buffer.Length, bytesInBuffer, bufferStartPointer, bytesToTransfer);
#endif
                if (bytesToTransfer == 0) {
                    return;
                }

                bufferStartPointer = dataStartPointer + 1;
                int bytesLeft = bytesInBuffer - bufferStartPointer;
#if DEBUG
                Log( "2 Chunk: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] Bytes to Transfer [{3}], BytesLeftInBuffer [{4}]", buffer.Length, bytesInBuffer, bufferStartPointer, bytesToTransfer, bytesLeft);
#endif
                while (bytesToTransfer > 0) {
#if DEBUG
                    Log(
                        "3 Chunk: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] Bytes to Transfer [{3}], BytesLeftInBuffer [{4}]",
                        buffer.Length, bytesInBuffer, bufferStartPointer, bytesToTransfer, bytesLeft);
#endif
                    //use up bytes in the existing buffer first
                    bytesLeft = bytesInBuffer - bufferStartPointer;

                    if (bytesLeft <= bytesToTransfer) {
#if DEBUG
                        Log( "4 Chunk: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] Bytes to Transfer [{3}], BytesLeftInBuffer [{4}]", buffer.Length, bytesInBuffer, bufferStartPointer, bytesToTransfer, bytesLeft);
#endif
                        Response.OutputStream.Write(buffer, bufferStartPointer, bytesLeft);
                        bytesToTransfer -= bytesLeft;

                        // read whats left
                        bytesInBuffer = networkStream.Read(buffer, 0, buffer.Length);
                        bufferStartPointer = 0;
                    }
                    else {
#if DEBUG
                        Log( "5 Chunk: BufferSize[{0}] BytesInBuffer[{1}] BufferStartPointer[{2}] Bytes to Transfer [{3}], BytesLeftInBuffer [{4}]", buffer.Length, bytesInBuffer, bufferStartPointer, bytesToTransfer, bytesLeft);
#endif
                        Response.OutputStream.Write(buffer, bufferStartPointer, bytesToTransfer);
                        bufferStartPointer += bytesToTransfer;
                        bytesToTransfer = 0;
                    }
                }
                bufferStartPointer += 2; // skip over the last two bytes...
            }
        }


        public void Log(string format, params object[] param) {
#if INTERACTIVE
            Console.Write("[{0}] ", System.Threading.Thread.CurrentThread.Name);
            Console.WriteLine(format, param);
#else
            EventLog.WriteEntry(string.Format(format,param),EventLogEntryType.Information);
#endif

        }

        public void NetworkStreamWrite(NetworkStream stream, string format, params object[] param) {
            byte[] outputBytes = Encoding.ASCII.GetBytes(string.Format(format, param));
            stream.Write(outputBytes, 0, outputBytes.Length);
        }

        public void NetworkStreamWriteLine(NetworkStream stream, string format, params object[] param) {
#if DEBUG
            Log(format, param);
#endif
            byte[] outputBytes = Encoding.ASCII.GetBytes(string.Format(format + "\r\n", param));
            stream.Write(outputBytes, 0, outputBytes.Length);
        }

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        public static void Main() {
            var svc = new SvnReverseProxyService();

            // to run this interactively (like, for debugging) make the following line true.
#if INTERACTIVE
            
            svc.OnStart(null);

#else // AS_SERVICE
            Run(svc);
#endif
        }
    }
}